package com.eternal.data.ui;

import android.annotation.SuppressLint;
import androidx.paging.AsyncPagedListDiffer;
import androidx.paging.PagedList;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;

import com.eternal.base.concat.TmpHum;
import com.eternal.framework.utils.KLog;

import org.reactivestreams.Subscription;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;

import io.reactivex.FlowableSubscriber;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.processors.PublishProcessor;
import io.reactivex.schedulers.Schedulers;

public class GraphAdapter {
    long minTime, dataMaxTime;
    int timeDistance;
    int minTmp, maxTmp, minHum, maxHum;
    int avgTmp, avgHum;
    int aMinTmp, aMaxTmp, aMinHum, aMaxHum;
    private PagedList<TmpHum> data;
    private List<TmpHum> snapshot;
    int size, length, positionOffset;
    private boolean ready;
    private List<Callback> callbacks;
    private PublishProcessor<Space> processor;
    private TimeCallback timeCallback;
    private DateFormat format;
    private DiffUtils util;

    public GraphAdapter() {
        ready = false;
        callbacks = new ArrayList<>(4);
        format = new SimpleDateFormat("h:mm a @ MMM dd", Locale.ENGLISH);
        initEvent();
        util = new DiffUtils();
        util.setListener(new DiffUtils.Listener() {
            @Override
            public void onEnd(PagedList<TmpHum> data, int addSize) {
                GraphAdapter.this.data = data;
                size = data.size();
                data.addWeakCallback(null, listener);
                positionOffset += addSize;
                snapshot = null;
                dataMaxTime = getActualItem(0).time;
                long temp;
                if(size>=2){
                    temp=getActualItem(1).time;
                }else{
                    temp=dataMaxTime;
                }
                if (temp < minTime + timeDistance) {
                    ready = false;
                    processor.onNext(new Space(minTime, timeDistance, true));
                    KLog.i("refresh history");
                } else {
                    KLog.i("no refresh history");
                }
            }
        });
    }

    private void initEvent() {
        processor = PublishProcessor.create();
        processor.onBackpressureLatest().map(new Function<Space, Integer>() {
            @Override
            public Integer apply(Space space) throws Exception {
                boolean isMax = dataMaxTime < space.min + space.distance / 2;
                if (timeDistance == space.distance && minTime == space.min && !space.update) {
                    return -1;
                }
                timeDistance = space.distance;
                if (size > 0 && isMax) {
                    minTime = dataMaxTime - space.distance / 2;
                } else {
                    minTime = space.min;
                }
                if (timeCallback != null) {
                    timeCallback.setStart(format.format(new Date(minTime * 1000L)));
                    timeCallback.setEnd(format.format(new Date((minTime + timeDistance) * 1000L)), isMax);
                }
                int position = positionOffset + length;
                updateSpace();
                return position;
            }
        }).subscribeOn(Schedulers.computation()).subscribe(new FlowableSubscriber<Integer>() {
            Subscription sub;

            @Override
            public void onSubscribe(Subscription s) {
                sub = s;
                sub.request(1);
            }

            @Override
            public void onNext(Integer integer) {
                if (integer >= 0) {
                    draw(integer, sub);
                } else {
                    sub.request(1);
                }
            }

            @Override
            public void onError(Throwable t) {
                t.printStackTrace();
                KLog.e(t);
            }

            @Override
            public void onComplete() {
                KLog.i("graph complete");
            }
        });
    }

    @SuppressLint("CheckResult")
    private void draw(final int position, final Subscription sub) {
        Observable.fromIterable(callbacks).flatMap(new Function<Callback, ObservableSource<Boolean>>() {
            @Override
            public ObservableSource<Boolean> apply(final Callback callback) throws Exception {
                return Observable.just(callback).map(new Function<Callback, Boolean>() {
                    @Override
                    public Boolean apply(Callback callback) {
                        callback.updatePath(position);
                        callback.draw();
                        return true;
                    }
                }).subscribeOn(Schedulers.io());
            }
        }).last(true).subscribe(new Consumer<Boolean>() {
            @Override
            public void accept(Boolean aBoolean) throws Exception {
                sub.request(1);
                ready = true;
            }
        });
    }

    public void submitList(final PagedList<TmpHum> data) {
        if (data == null) {
            return;
        }
        KLog.i("recive new list");

        if (this.data != null) {
            snapshot = this.data.snapshot();
            this.data.removeWeakCallback(listener);
            this.data = null;
            util.init(snapshot, data);
        } else if (snapshot != null) {
            util.init(snapshot, data);
        } else {
            ready = false;
            this.data = data;
            size = data.size();
            data.addWeakCallback(null, listener);
            long now = System.currentTimeMillis() / 1000;
            if (size == 0) {
                dataMaxTime = now;
            } else {
                dataMaxTime = getActualItem(0).time;
            }
            int distance = timeDistance;
            setVisibleTimeSpace(now >= dataMaxTime + distance / 2 ? dataMaxTime : now - distance, distance);
        }
    }

    public boolean isReady() {
        return ready;
    }

    public boolean isEnd() {
        return dataMaxTime - timeDistance / 2 * 3 < minTime;
    }

    public long getMinTime() {
        return minTime * 1000L;
    }

    private TmpHum getActualItem(int position) {
        if (data == null) {
            return snapshot.get(position);
        }
        data.loadAround(position);
        return data.get(position);
    }

    public void addCallback(Callback callback) {
        callbacks.add(callback);
    }

    public void setVisibleTimeSpace(long newMin, int distance) {
        if (data == null && snapshot == null) {
            return;
        }
        ready = false;
        processor.onNext(new Space(newMin, distance, false));
    }

    public void setDistance(int distance) {
        if (minTime == dataMaxTime && distance == timeDistance) {
            return;
        }
        int now = (int) (System.currentTimeMillis() / 1000);
        minTime = now >= dataMaxTime + distance / 2 ? dataMaxTime : now - distance;
        timeDistance = distance;
        if (data != null) {
            processor.onNext(new Space(minTime, distance, true));
        }
    }

    public void setMinTime(int time) {
        KLog.e("is ready :" + ready);
        if (dataMaxTime - timeDistance / 2 >= time) {
            minTime = time;
            if (data != null) {
                processor.onNext(new Space(time, timeDistance, true));
            }
        }
    }

    public void setTimeCallback(TimeCallback timeCallback) {
        this.timeCallback = timeCallback;
    }

    private void initOffsetAndLength() {
        long maxTime = minTime + timeDistance;
        if (data == null) {
            size = snapshot.size();
        } else {
            size = data.size();
        }
        while (positionOffset < size && getActualItem(positionOffset).time > maxTime) {
            ++positionOffset;
        }
        while (positionOffset > 0 && getActualItem(positionOffset - 1).time <= maxTime) {
            --positionOffset;
        }
        if (positionOffset > 0) {
            --positionOffset;
        }
        length = 0;
        for (int i = positionOffset; i < size && getActualItem(i).time >= minTime; i++) {
            ++length;
        }
        if (length + positionOffset < size) {
            ++length;
        }
    }

    public int getDistance() {
        switch (timeDistance) {
            case 3600:
                return 60;
            case 86400:
                return 60 * 12;
            case 604800:
                return 3600;
            case 2678400:
                return 3600 * 4;
            case 31536000:
                return 3600 * 24;
            default:
                return 0;
        }
    }

    private void updateSpace() {
        initOffsetAndLength();
        if (positionOffset >= size - 1) {
            length = 0;
            maxTmp = 0;
            maxHum = 0;
            minTmp = 0;
            minHum = 0;
            avgTmp = 0;
            avgHum = 0;
            aMaxHum = 0;
            aMinHum = 0;
            aMaxTmp = 0;
            aMinTmp = 0;
            return;
        }
        int dis = getDistance(), size;
        long maxTime = minTime + timeDistance;
        long hum, tmp, lt;
        TmpHum now, last;
        last = getItem(0);
        now = getItem(1);
        lt = last.time;
        minTmp = last.getTmpValue();
        minHum = last.hum;
        if (last.time > maxTime) {
            minTmp = (int) (((maxTime - last.time) / (float) (now.time - last.time) * (now.getTmpValue() - last.getTmpValue())) + last.getTmpValue());
            minHum = (int) (((maxTime - last.time) / (float) (now.time - last.time) * (now.hum - last.hum)) + last.hum);
        }
        maxHum = minHum;
        maxTmp = minTmp;
        aMaxTmp = minTmp;
        aMinTmp = minTmp;
        aMaxHum = minHum;
        aMinHum = minHum;
        tmp = minTmp;
        hum = minHum;
        size = 1;
        for (int i = 1; i < length - 1; i++) {
            now = getItem(i);
            if (maxHum < now.hum) {
                maxHum = now.hum;
            } else if (minHum > now.hum) {
                minHum = now.hum;
            }
            if (maxTmp < now.getTmpValue()) {
                maxTmp = now.getTmpValue();
            } else if (minTmp > now.getTmpValue()) {
                minTmp = now.getTmpValue();
            }
            if (lt - now.time >= dis) {
                if (aMaxHum < now.hum) {
                    aMaxHum = now.hum;
                } else if (aMinHum > now.hum) {
                    aMinHum = now.hum;
                }
                if (aMaxTmp < now.getTmpValue()) {
                    aMaxTmp = now.getTmpValue();
                } else if (aMinTmp > now.getTmpValue()) {
                    aMinTmp = now.getTmpValue();
                }
                hum += now.hum;
                tmp += now.getTmpValue();
                lt = now.time;
                ++size;
            }
            last = now;
        }
        now = getItem(length - 1).clone();
        if (now.time < minTime) {
            now.tmp = (int)((minTime - last.time) / (float) (now.time - last.time) * (now.getTmpValue() - last.getTmpValue()) + last.getTmpValue());
            now.hum = ((char) ((minTime - last.time) / (float) (now.time - last.time) * (now.hum - last.hum) + last.hum));
            now.time = minTime;
        }
        if (maxHum < now.hum) {
            maxHum = now.hum;
        } else if (minHum > now.hum) {
            minHum = now.hum;
        }
        if (maxTmp < now.getTmpValue()) {
            maxTmp = now.getTmpValue();
        } else if (minTmp > now.getTmpValue()) {
            minTmp = now.getTmpValue();
        }

        if (aMaxHum < now.hum) {
            aMaxHum = now.hum;
        } else if (aMinHum > now.hum) {
            aMinHum = now.hum;
        }
        if (aMaxTmp < now.getTmpValue()) {
            aMaxTmp = now.getTmpValue();
        } else if (aMinTmp > now.getTmpValue()) {
            aMinTmp = now.getTmpValue();
        }
        ++size;
        hum += now.hum;
        tmp += now.getTmpValue();
        avgHum = (char) (hum / size);
        avgTmp = (int) tmp / size;
    }

    int nearestSampleIndexForX(int width, float x) {
        int time = timeFromX(width, x);
        if (length == 0) {
            return -1;
        }
        if (time > dataMaxTime) {
            return 0;
        }
        if (time < getItem(length - 1).time) {
            return length - 1;
        }
        TmpHum temp, last = null;
        for (int i = 0; i < length; ++i) {
            temp = getItem(i);

            if (temp.time == time) {
                return i;
            }
            if (i > 0 && temp.time < time && last.time > time) {
                return (time - temp.time < last.time - time) ? i : i - 1;
            }
            last = temp;
        }
        return -1;
    }

    TmpHum getItem(int position) {
        if (data == null) {
            return snapshot.get(position + positionOffset);
        }
        return data.get(position + positionOffset);
    }

    int timeFromX(int width, float x) {
        if (width > 0) {
            return (int) (((int) (x * timeDistance / width)) + minTime);
        }
        return 0;
    }

    float xFromTime(int width, long time) {
        return (float) (time - minTime) * width / timeDistance;
    }

    boolean isInit() {
        return data != null || snapshot != null;
    }

    private PagedList.Callback listener = new PagedList.Callback() {
        @Override
        public void onChanged(int position, int count) {
            KLog.e("data change position:" + position + " count:" + count);
        }

        @Override
        public void onInserted(int position, int count) {
            ready = false;
            processor.onNext(new Space(minTime, timeDistance, true));
        }

        @Override
        public void onRemoved(int position, int count) {
            KLog.e("data removed position:" + position + " count" + count);
        }
    };

    public interface Callback {
        void updatePath(int position);

        void draw();
    }

    public interface TimeCallback {
        void setStart(String time);

        void setEnd(String time, boolean isEnd);
    }

    public static class Space {
        final long min;
        final int distance;
        final boolean update;

        public Space(long min, int distance, boolean update) {
            this.min = min;
            this.distance = distance;
            this.update = update;
        }
    }
}